import functools
from functools import reduce
from io import BytesIO
from typing import List, Type, Dict, Optional, Union

from emi import io
from emi.bar.storage.model import BaseDBModel
from emi.bar.db import  RankDB
from emi.core.DBSource import DBSource
from emi.util import  TimeUtils
from emi.bar.data import Interval, RankBar, RankData
import peewee
from datetime import datetime


def _get_detail_bytes(bar:RankBar)->bytes:
    rank_data_list = bar.details()
    if not rank_data_list:
        return None
    hh_mm_ss_list = []
    value_list = []
    extra_list:List[bytes] = []
    order_list = []
    value_avg_list = []
    count_list = []

    for ran_data in rank_data_list:
        y_m_d, h_m_s = TimeUtils.split_datetime(ran_data.datetime)
        hh_mm_ss_list.append(h_m_s)
        value_list.append(ran_data.value)
        extra_list.append(ran_data.get_extra_bytes())
        order_list.append(ran_data.order)
        value_avg_list.append(ran_data.value_avg)
        count_list.append(ran_data.count)
    output = BytesIO()
    io.write_int_compress(output, hh_mm_ss_list)
    io.write_float_compress(output, value_list)
    io.write_bytes_list(output, extra_list)
    io.write_int_compress(output, order_list)
    io.write_float_compress(output, value_avg_list)
    io.write_int_compress(output, count_list)
    return output.getvalue()

def _load_detail_bytes(bar:RankBar,data:bytes)->List[RankData]:
    offset = 0
    hh_mm_ss_list,offset = io.read_int_compress(data,offset)
    value_list,offset = io.read_float_compress(data,offset)
    extra_list,offset = io.read_bytes_list(data,offset)
    order_list,offset = io.read_int_compress(data,offset)
    value_avg_list,offset = io.read_float_compress(data,offset)
    count_list,offset = io.read_int_compress(data,offset)
    rank_data_list = []
    for i in range(0,len(value_list)):
        hh_mm_ss = hh_mm_ss_list[i]
        hour = int(hh_mm_ss / 10000 + 0.01)
        minute = int((hh_mm_ss / 100) % 100 + 0.01)
        second = int(hh_mm_ss % 100 + 0.01)
        time = bar.datetime.replace(hour=hour,minute=minute,second=second)

        rank_data = RankData(symbol=bar.symbol,
                             datetime=time,
                             category_code=bar.category_code,
                             value=value_list[i],
                             _extra=extra_list[i],
                             _order=order_list[i],
                             _value_avg=value_avg_list[i],
                             _count=count_list[i])
        rank_data_list.append(rank_data)
    return rank_data_list



class RankBarModel(BaseDBModel):

    symbol: str = peewee.CharField(index=True,max_length=64)
    yy_mm_dd:int = peewee.SmallIntegerField(index=True)
    interval: int = peewee.SmallIntegerField(index=True)
    name:str = peewee.CharField(index=False,max_length=64,default="")
    category_code:str = peewee.CharField(index=True,max_length=64,null=False)
    order: int = peewee.IntegerField(index=False)
    count: int = peewee.IntegerField(index=False)
    value: int = peewee.IntegerField(index=False)
    value_avg: int = peewee.IntegerField(index=False)
    extra_data: bytes = peewee.BlobField(null=True)
    detail_data:bytes = peewee.BlobField(null=True)

    @classmethod
    def get_field(cls,detail:bool):
        db_model = cls
        if detail:
            return  (
                db_model.symbol, db_model.yy_mm_dd, db_model.interval, db_model.order,
                db_model.count, db_model.value, db_model.value_avg, db_model.extra_data,
                db_model.name,db_model.category_code,
                db_model.detail_data)
        else:
            return (
                db_model.symbol, db_model.yy_mm_dd, db_model.interval, db_model.order,
                db_model.count, db_model.value, db_model.value_avg, db_model.extra_data,
                db_model.name, db_model.category_code,
            )


    def to_dict(self):
        return self.__data__

    @staticmethod
    def from_data(bar: RankBar):

        ## version和serial_id是压缩的。
        model = RankBarModel()
        model.symbol = bar.symbol
        model.interval = bar.interval.value
        y_m_d, h_m_s = TimeUtils.split_datetime(bar.datetime)
        model.yy_mm_dd = y_m_d
        if not bar.category_code:
            model.category_code = ""
        else:
            model.category_code = bar.category_code
        model.name = bar.name
        model.order = bar._order
        model.count = bar._count
        model.value = bar.value
        model.value_avg = bar._value_avg
        model.extra_data = bar.get_extra_bytes()
        model.detail_data = _get_detail_bytes(bar)
        return model


    def to_bar(self)->RankBar:
        interval = Interval(self.interval)
        year = int(self.yy_mm_dd / 10000 + 0.01)
        month = int((self.yy_mm_dd / 100 + 0.01)) % 100
        day = self.yy_mm_dd % 100
        category_code = self.category_code if self.category_code else None  ## 空字符串为None
        time = datetime(year=year, month=month, day=day, hour=15, minute=0, second=0)
        bar = RankBar(interval = interval,
                      datetime = time,
                      symbol = self.symbol,
                      category_code=category_code,
                      name=self.name,
                      _order=self.order,
                      _count=self.count,
                      value=self.value,
                      _value_avg=self.value_avg,
                      _extra=self.extra_data,
                      )
        if self.detail_data:
            ## 延迟加载。
            bar._lazy_load_fun = functools.partial(_load_detail_bytes, bar, self.detail_data)
        return bar



class RankStorage(RankDB):

    def __init__(self,source:DBSource,table_name = None):

        if table_name is None:
            table_name = 'rank_db_default'
        indexes_desc = (
            (("symbol", "yy_mm_dd", "interval","category_code"), True),
        )
        self.source = source.init_peewee(table_name,RankBarModel,indexes=indexes_desc,ignore_inited=True)

    def save_rank(self,bars:List[RankBar]):
        """
        保存排名情况。
        :param bars:
        :return:
        """
        models = [ RankBarModel.from_data(bar) for bar in bars]
        with self.source.use_peewee() as db_model:
            db_model.save_all(models)

    def clear_at_(self, time,interval:Interval):
        yy_mm_dd, h_m_s = TimeUtils.split_datetime(time)
        with self.source.use_peewee() as db_model:
            db_model.delete().where(
                (db_model.yy_mm_dd == yy_mm_dd)
                &(db_model.interval == interval.value)
            ).execute()


    def clear(self, symbol: str = None):
        with self.source.use_peewee() as db_model:
            if not symbol is None:
                db_model.delete().where(
                    (db_model.symbol == symbol)
                ).execute()
            else:
                ##情况表
                db_model.drop_table()
                db_model.create_table()

    def size(self,symbol:str = None)->int:
        with self.source.use_peewee() as db_model:
            if symbol is None:
                return db_model.select().count()
            else:
                return db_model.select().where( db_model.symbol == symbol).count()

    def get_day_list(self,symbol:str= None,interval:Interval = None) -> List[datetime]:
        with self.source.use_peewee() as db_model:
            if interval is None:
                if symbol is None:
                    s = (db_model.select(db_model.yy_mm_dd)
                         .order_by(db_model.yy_mm_dd.asc()).distinct())
                else:
                    s = (
                        db_model.select(db_model.yy_mm_dd).where(
                            (db_model.symbol == symbol)
                        ).order_by(db_model.yy_mm_dd.asc()).distinct()
                    )
            else:
                interval = interval.value
                if symbol is None:
                    s = (db_model.select(db_model.yy_mm_dd).where(
                        (db_model.interval == interval)
                    ).order_by(db_model.yy_mm_dd.asc()).distinct())
                else:
                    s = (
                        db_model.select(db_model.yy_mm_dd).where(
                            (db_model.symbol == symbol)
                            & (db_model.interval == interval)
                        ).order_by(db_model.yy_mm_dd.asc()).distinct()
                    )
            yy_mm_dd_list = [db_bar.yy_mm_dd for db_bar in s]
        return [ _to_datetime(yy_mm_dd)  for yy_mm_dd in yy_mm_dd_list]


    def get_symbol_list(self) -> List[str]:
        with self.source.use_peewee() as db_model:
            s = (db_model.select(db_model.symbol).distinct())
            return [v.symbol for v in s]


    def get_rank(self, symbol: str, detail=True, interval:Interval = Interval.DAILY,
                 start: datetime = None,
                 end: datetime = None) -> List[RankBar]:
        """
        返回个股的排名情况。
        :param symbol:
        :param detail:是否包含详情数据
        :param start:
        :param end:
        :return:
        """
        if start is None:
            start_y_m_d = 0
        else:
            start_y_m_d, h_m_s = TimeUtils.split_datetime(start)
        if end is None:
            end = datetime.now()
        end_y_m_d, h_m_s = TimeUtils.split_datetime(end)
        interval = interval.value
        with self.source.use_peewee() as db_model:
            s = (
                db_model.select(*db_model.get_field(detail))
                    .where(
                    (db_model.symbol == symbol)
                    & (db_model.interval == interval)
                    & (db_model.yy_mm_dd >= start_y_m_d)
                    & (db_model.yy_mm_dd <= end_y_m_d)
                )
                    .order_by(db_model.yy_mm_dd)
            )
            data = [db_bar.to_bar() for db_bar in s]
            return data

    def get_rank_recent(self, symbol: str, time: datetime, count: int, detail=True, is_before=True,
                        interval: Interval = Interval.DAILY) -> List[RankBar]:
        _y_m_d, h_m_s = TimeUtils.split_datetime(time)
        assert interval is not None
        interval = interval.value

        with self.source.use_peewee() as db_model:
            fields =db_model.get_field(detail)
            if is_before:
                s = (
                    db_model.select(*fields)
                        .where(
                        (db_model.symbol == symbol)
                        & (db_model.interval == interval)
                        & (db_model.yy_mm_dd <= _y_m_d)
                    )
                        .order_by(db_model.yy_mm_dd.desc())
                        .limit(count)
                )
                data = [db_bar.to_bar() for db_bar in s]
                return list(reversed(data))
            else:
                s = (
                    db_model.select(*fields)
                        .where(
                        (db_model.symbol == symbol)
                        & (db_model.interval == interval)
                        & (db_model.yy_mm_dd >= _y_m_d)
                    )
                        .order_by(db_model.yy_mm_dd.asc())
                        .limit(count)
                )
                data = [db_bar.to_bar() for db_bar in s]
                return data

    def get_best(self, time: datetime, detail=True,
                       interval: Interval = Interval.DAILY, max_order: int = None, min_value: float = None,
                       min_count: int = None) -> List[RankBar]:
        """
        返回最佳排名值。
        :param time:
        :param detail:
        :param max_order:
        :param min_value:
        :param min_count:
        :return:
        """
        yy_mm_dd, h_m_s = TimeUtils.split_datetime(time)
        if max_order is None:
            max_order = 999999
        if min_value is None:
            min_value = -999999
        if min_count is None:
            min_count = 0

        with self.source.use_peewee() as db_model:
            s = (
                db_model.select(*db_model.get_field(detail))
                    .where(
                    (db_model.yy_mm_dd == yy_mm_dd)
                    & (db_model.interval == interval)
                    & (db_model.order <= max_order)
                    & (db_model.value >= min_value)
                    & (db_model.count >= min_count)
                )
                    .order_by(db_model.value.desc())
            )
            data = [db_bar.to_bar() for db_bar in s]
        return data

    def get_rank_at(self, time, symbol: Union[str, List[str]] = None, detail=True,
                    interval: Interval = Interval.DAILY, as_map=False) -> Union[Dict[str,RankBar], List[RankBar]]:

        yy_mm_dd, h_m_s = TimeUtils.split_datetime(time)
        with self.source.use_peewee() as db_model:
            if symbol is None:
                s = (
                    db_model.select(*db_model.get_field(detail))
                        .where(
                        (db_model.yy_mm_dd == yy_mm_dd)
                        & (db_model.interval == interval)
                    )
                )
                list_data = [db_bar.to_bar() for db_bar in s]
                if as_map:
                    ret = {}
                    for item in list_data:
                        ret[item.symbol] = item
                    return ret
                return list_data
            elif isinstance(symbol,str):
                s = (
                    db_model.select(*db_model.get_field(detail))
                        .where(
                        (db_model.yy_mm_dd == yy_mm_dd)
                        & (db_model.interval == interval)
                        & (db_model.symbol == symbol)
                    )
                )
                list_data = [db_bar.to_bar() for db_bar in s]
                if as_map:
                    ret = {}
                    for item in list_data:
                        ret[item.symbol] = item
                    return ret
                return list_data
            else:
                s = (
                    db_model.select(*db_model.get_field(detail))
                        .where(
                        (db_model.yy_mm_dd == yy_mm_dd)
                        & (db_model.interval == interval)
                        & (db_model.symbol.in_(symbol))
                    )
                )
                list_data = [db_bar.to_bar() for db_bar in s]
                if as_map:
                    ret = {}
                    for item in list_data:
                        ret[item.symbol] = item
                    return ret
                return list_data

    def get_newest_rank(self, symbol: Union[str, List[str]]=None, detail=True, interval: Interval = Interval.DAILY) ->List[RankBar]:
        """
        获取最新的排名
        :param symbol: 单个或者多个。
        :param detail:
        :return:
        """
        day_list = self.get_day_list()
        if not day_list:
            return []

        return self.get_rank_at(day_list[-1],symbol,detail,interval)

    def get_newest_best(self, detail=True, interval: Interval = Interval.DAILY, max_order: int = None,
                        min_value: float = None, min_count: int = None) -> List[RankBar]:
        day_list = self.get_day_list()
        if not day_list:
            return []
        return self.get_best(day_list[-1],detail,interval,max_order,min_value,min_count)




def _to_datetime(yy_mm_dd:int,the_day_end = False)->datetime:
    year = int(yy_mm_dd / 10000 + 0.01)
    month = int((yy_mm_dd / 100 + 0.01)) % 100
    day = yy_mm_dd % 100
    if the_day_end:
        return datetime(year=year, month=month, day=day, hour=23, minute=59, second=59)
    else:
        return datetime(year=year, month=month, day=day, hour=0, minute=0, second=0)
